<?php
/**
 * Created by PhpStorm.
 * User: boge
 * Date: 2017/2/7
 * Time: 11:09
 */

namespace net\socket;

require("WebSocketParser.php");
require("Binary.php");


use net\parser\WebSocketParser;

class WebSocket
{

    const VERSION = '0.1.4';
    const TOKEN_LENGHT = 16;
    /** 缓存的上次发送时间 */
    private $cacheInt = 0;

    protected $socket;
    protected $key;
    protected $host;
    protected $port;
    protected $path = "/";
    /** 是否已经连接 */
    protected $connected = false;
    /** 是否已经握手 */
    protected $handshake = false;
    /** 连接锁 */
    protected $sock;
    /** 服务器返回的头 */
    protected $header;

    public $parser;

    const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

    public function __construct($host = '127.0.0.1', $port = 8085, $path = '/')
    {

        $this->host = $host;
        $this->port = $port;
        $this->key = $this->generateToken(self::TOKEN_LENGHT);
        $this->parser = new WebSocketParser();

    }

    public function connect() {
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if ($this->socket < 0) {
            echo "socket_create() failed: reason: " . socket_strerror($this->socket) . "\n";
        } else {
            echo "OK.\n";
        }
        echo "试图连接 '$this->host' 端口 '$this->port'...\n";


        $this->sock = socket_connect($this->socket, $this->host, $this->port);
        if(!$this->sock){
            echo "连接失败";
            socket_close($this->socket);
            exit;
        }
        if ($this->sock < 0) {
            echo "socket_connect() failed.\nReason: ($this->sock) " . socket_strerror($this->sock) . "\n";
        } else {
            echo "连接OK\n";
            $this->connected = true;
            //WebSocket握手
            socket_write($this->socket, $this->createHeader());

            while($this->sock){
                $temp = socket_read($this->socket, 8192);
                if ($temp) {
                    $headerBuffer = $temp;
                    if (substr($headerBuffer, -4, 4) != "\r\n\r\n")
                    {
                        continue;
                    }
                } else {
                    return false;
                }
                return $this->doHandShake($headerBuffer);
            }
        }
    }

    /**
     * 接收数据
     * @param int $length 接收数据的长度
     * @param bool $waitall 等待接收到全部数据后再返回，注意这里超过包长度会阻塞住
     * @return string | bool
     */
    function recv($length = 65535, $waitall = false)
    {
        if (!$this->handshake)
        {
            trigger_error("not complete handshake.");
            return false;
        }
        $flags = 0;
        if ($waitall)
        {
            $flags = MSG_WAITALL;
        }
        while (true)
        {
            $ret = socket_recv($this->socket, $data, $length, $flags);
            if ($ret === false)
            {
                return false;
            }
            if (!$data)
            {
                return false;
            }
            $this->parser->push($data);
            $frame = $this->parser->pop($data);
            if ($frame)
            {
                $byte = gzuncompress($frame->data);
                $input = new \Binary($byte);
                return $input;
            }
        }
        return false;
    }

    /**
     * 发送数据到服务器
     * @param $action 动作
     * @param $data 数据 Binary
     */
    public function sendData($action, $data=null) {
			if ($this->socket && $this->connected) {
			    $output = new \Binary();
                $output->addLong($this->getVerify());
                $output->addInt($action);
                if (empty($data)) {
                    $output->addInt(0);
                } else {
                    $output->addInt(strlen($data->body));
                    $output->body .= $data->body;
                }
//				var_dump("成功发送：".strlen($output->body));
				// 加密
//				var encryptedBytes:ByteBuffer = aes.encrypt(byte);
//				byte.clear();
//				byte.writeUTFBytes(Hex.fromArray(encryptedBytes));
				// 压缩
                $output->body = gzcompress($output->body);
//                var_dump("发送数据=".$output->body);
				$this->send($output->body, "bin");
//				trace("成功发送：", byte.length);
			}
    }

    private function getVerify() {
        $timer = time()*1000;
        if ($timer <= $this->cacheInt) {
            $timer = $this->cacheInt+1;
        }
        $this->cacheInt = $timer;
        return $this->cacheInt;
    }

    /**
     * send string data
     * @param $data
     * @param string $type
     * @param bool $masked
     * @throws \Exception
     * @return bool
     */
    public function send($data, $type = 'text', $masked = true)
    {
        if (empty($data))
        {
            throw new \Exception("data is empty");
        }
        if (!$this->handshake)
        {
            trigger_error("not complete handshake.");
            return false;
        }
        $_send = $this->hybi10Encode($data, $type, $masked);
        return socket_write($this->socket, $_send);
    }

    /**
     * @param $payload
     * @param string $type
     * @param bool $masked
     * @return bool|string
     */
    private function hybi10Encode($payload, $type = 'text', $masked = true)
    {
        $frameHead = array();
        $payloadLength = strlen($payload);
        switch ($type)
        {
            //文本内容
            case 'text':
                // first byte indicates FIN, Text-Frame (10000001):
                $frameHead[0] = 129;
                break;
            //二进制内容
            case 'binary':
            case 'bin':
                // first byte indicates FIN, Text-Frame (10000010):
                $frameHead[0] = 130;
                break;
            case 'close':
                // first byte indicates FIN, Close Frame(10001000):
                $frameHead[0] = 136;
                break;
            case 'ping':
                // first byte indicates FIN, Ping frame (10001001):
                $frameHead[0] = 137;
                break;
            case 'pong':
                // first byte indicates FIN, Pong frame (10001010):
                $frameHead[0] = 138;
                break;
        }
        // set mask and payload length (using 1, 3 or 9 bytes)
        if ($payloadLength > 65535)
        {
            $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8);
            $frameHead[1] = ($masked === true) ? 255 : 127;
            for ($i = 0; $i < 8; $i++)
            {
                $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
            }
            // most significant bit MUST be 0 (close connection if frame too big)
            if ($frameHead[2] > 127)
            {
                $this->disconnect();
                return false;
            }
        }
        elseif ($payloadLength > 125)
        {
            $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8);
            $frameHead[1] = ($masked === true) ? 254 : 126;
            $frameHead[2] = bindec($payloadLengthBin[0]);
            $frameHead[3] = bindec($payloadLengthBin[1]);
        }
        else
        {
            $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
        }
        // convert frame-head to string:
        foreach (array_keys($frameHead) as $i)
        {
            $frameHead[$i] = chr($frameHead[$i]);
        }
        // generate a random mask:
        $mask = array();
        if ($masked === true)
        {
            for ($i = 0; $i < 4; $i++)
            {
                $mask[$i] = chr(rand(0, 255));
            }
            $frameHead = array_merge($frameHead, $mask);
        }
        $frame = implode('', $frameHead);
        // append payload to frame:
        for ($i = 0; $i < $payloadLength; $i++)
        {
            $frame .= $masked ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
        }
        return $frame;
    }


    private function createHeader() {
        $host = $this->host;
        if ($host === '127.0.0.1' || $host === '0.0.0.0') {
            $host = 'localhost';
        }

        $origin = "null";

        return
            "GET {$this->path} HTTP/1.1" . "\r\n" .
            "Origin: {$origin}" . "\r\n" .
            "Host: {$host}:{$this->port}" . "\r\n" .
            "Sec-WebSocket-Key: {$this->key}" . "\r\n" .
            "User-Agent: PHPWebSocketClient/" . self::VERSION . "\r\n" .
            "Upgrade: websocket" . "\r\n" .
            "Connection: Upgrade" . "\r\n" .
            "Sec-WebSocket-Protocol: wamp" . "\r\n" .
            "Sec-WebSocket-Version: 13" . "\r\n" . "\r\n";
    }

    /**
     * Generate token
     *
     * @param int $length
     * @return string
     */
    private function generateToken($length)
    {
        $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"§$%&/()=[]{}';
        $useChars = array();
        // select some random chars:
        for ($i = 0; $i < $length; $i++) {
            $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)];
        }
        // Add numbers
        array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9));
        shuffle($useChars);
        $randomString = trim(implode('', $useChars));
        $randomString = substr($randomString, 0, self::TOKEN_LENGHT);
        return base64_encode($randomString);
    }

    /**
     * 握手
     * @param $headerBuffer
     * @return bool
     */
    function doHandShake($headerBuffer)
    {
        $header = $this->parseHeader($headerBuffer);
        if (!isset($header['Sec-WebSocket-Accept']))
        {
            $this->disconnect();
            return false;
        }
        if ($header['Sec-WebSocket-Accept'] != base64_encode(pack('H*', sha1($this->key . self::GUID))))
        {
            $this->disconnect();
            return false;
        }
        $this->handshake = true;
        $this->header = $header;
        return true;
    }

    /**
     * Disconnect from server
     */
    public function disconnect()
    {
        $this->connected = false;
        $this->sock = 0;
        socket_close($this->socket);
        socket_shutdown($this->socket);
    }

    /**
     * 头部解析
     * @param $data
     * @return array
     */
    private function parseHeader($data)
    {
        $header = array();
        $header[0] = array();
        $meta = &$header[0];
        $parts = explode("\r\n\r\n", $data, 2);
        // parts[0] = HTTP头;
        // parts[1] = HTTP主体，GET请求没有body
        $headerLines = explode("\r\n", $parts[0]);
        // HTTP协议头,方法，路径，协议[RFC-2616 5.1]
        list($meta['method'], $meta['uri'], $meta['protocol']) = explode(' ', $headerLines[0], 3);
        //错误的HTTP请求
        if (empty($meta['method']) or empty($meta['uri']) or empty($meta['protocol']))
        {
            return false;
        }
        unset($headerLines[0]);
        //解析Header
        $header = array_merge($header, self::parseHeaderLine($headerLines));
        return $header;
    }

    /**
     * 传入一个字符串或者数组
     * @param $headerLines string/array
     * @return array
     */
    private function parseHeaderLine($headerLines)
    {
        if (is_string($headerLines))
        {
            $headerLines = explode("\r\n", $headerLines);
        }
        $header = array();
        foreach ($headerLines as $_h)
        {
            $_h = trim($_h);
            if (empty($_h)) continue;
            $_r = explode(':', $_h, 2);
            // 头字段名称首字母大写
            $keys = explode('-', $_r[0]);
            $keys = array_map("ucfirst", $keys);
            $key = implode('-', $keys);
            $value = isset($_r[1])?$_r[1]:'';
            $header[trim($key)] = trim($value);
        }
        return $header;
    }


}